1use crate::copp::clarabel_backend::ConstraintsClarabel;
30use crate::copp::copp3::Copp3Result;
31use crate::copp::copp3::formulation::{Topp3Problem, get_weight_a_topp3};
32use crate::copp::copp3::opt3::clarabel_constraints::{
33 clarabel_standard_capacity_topp3, clarabel_standard_constraint_topp3,
34};
35use crate::copp::{ClarabelOptions, clarabel_to_copp3_solution};
36use crate::diag::{
37 CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
38 check_boundary_state_copp3_valid, check_s_interval_valid, format_duration_human,
39};
40use clarabel::algebra::CscMatrix;
41use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT};
42use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
43
44pub fn topp3_socp(
63 problem: &Topp3Problem,
64 options: &ClarabelOptions,
65) -> Result<Copp3Result, CoppError> {
66 let (result, solution) = topp3_socp_expert(problem, options)?;
67 result.ok_or_else(|| CoppError::ClarabelSolverStatus("topp3_socp".into(), solution.status))
68}
69
70pub fn topp3_socp_expert(
94 problem: &Topp3Problem,
95 options: &ClarabelOptions,
96) -> Result<(Option<Copp3Result>, DefaultSolution<f64>), CoppError> {
97 match options.verbosity() {
98 Verbosity::Silent => topp3_socp_core(problem, (options, SilentVerboser)),
99 Verbosity::Summary => topp3_socp_core(problem, (options, SummaryVerboser::new())),
100 Verbosity::Debug => topp3_socp_core(problem, (options, DebugVerboser::new())),
101 Verbosity::Trace => topp3_socp_core(problem, (options, TraceVerboser::new())),
102 }
103}
104
105fn topp3_socp_core(
117 problem: &Topp3Problem,
118 options_verboser: (&ClarabelOptions, impl Verboser),
119) -> Result<(Option<Copp3Result>, DefaultSolution<f64>), CoppError> {
120 let (options, mut verboser) = options_verboser;
121 let idx_s_start = problem.idx_s_start;
122 let a_boundary = problem.a_boundary;
123 let b_boundary = problem.b_boundary;
124 let num_stationary = problem.num_stationary;
125 if verboser.is_enabled(Verbosity::Summary) {
126 verboser.record_start_time();
127 }
128 if verboser.is_enabled(Verbosity::Trace) {
129 let settings = options.clarabel_settings();
130 crate::verbosity_log!(
131 crate::diag::Verbosity::Summary,
132 "topp3_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
133 options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
134 options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
135 options.is_allow(clarabel::solver::SolverStatus::MaxTime),
136 options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
137 options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
138 settings.tol_gap_rel,
139 settings.tol_feas,
140 settings.max_iter,
141 settings.verbose
142 );
143 }
144
145 check_boundary_state_copp3_valid(a_boundary, b_boundary)?;
147 let n = problem.a_linearization.len() - 1;
148 let idx_s_final = idx_s_start + n;
149 if verboser.is_enabled(Verbosity::Summary) {
150 crate::verbosity_log!(
151 crate::diag::Verbosity::Summary,
152 "\ntopp3_socp started: {} <= idx_s <= {}, s_len = {}, num_stationary={:?}.",
153 idx_s_start,
154 idx_s_final,
155 problem.a_linearization.len(),
156 num_stationary
157 );
158 }
159 check_s_interval_valid("topp3_socp", idx_s_start, idx_s_final)?;
160 let len_xi = length_xi_eta(n, num_stationary);
161 let id_xi_start = 2 * (n + 1);
162 let id_eta_start = id_xi_start + len_xi;
163 let (cap_val_lp, cap_b_lp, cap_cone_lp) =
173 clarabel_standard_capacity_topp3(problem.constraints, (idx_s_start, idx_s_final));
174 let (cap_val_qp, cap_b_qp, cap_cone_qp) = clarabel_capacity_topp3_qp(n);
175 if verboser.is_enabled(Verbosity::Debug) {
176 crate::verbosity_log!(
177 crate::diag::Verbosity::Summary,
178 "topp3_socp: capacity estimate lp(val={cap_val_lp}, b={cap_b_lp}, cone={cap_cone_lp}), qp(val={cap_val_qp}, b={cap_b_qp}, cone={cap_cone_qp}), n_var={}",
179 id_eta_start + len_xi
180 );
181 }
182 let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_lp + cap_cone_qp);
183 let mut row = Vec::<usize>::with_capacity(cap_val_lp + cap_val_qp);
184 let mut col = Vec::<usize>::with_capacity(cap_val_lp + cap_val_qp);
185 let mut val = Vec::<f64>::with_capacity(cap_val_lp + cap_val_qp);
186 let mut b = Vec::<f64>::with_capacity(cap_b_lp + cap_b_qp);
187 if verboser.is_enabled(Verbosity::Trace) {
188 crate::verbosity_log!(
189 crate::diag::Verbosity::Summary,
190 "topp3_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
191 cap_val_lp + cap_val_qp,
192 cap_val_lp + cap_val_qp,
193 cap_val_lp + cap_val_qp,
194 cap_b_lp + cap_b_qp,
195 cap_cone_lp + cap_cone_qp
196 );
197 }
198
199 let s = problem.constraints.s_vec(idx_s_start, idx_s_final + 1)?;
201 let row_before_std = row.len();
202 let col_before_std = col.len();
203 let val_before_std = val.len();
204 let b_before_std = b.len();
205 let cones_before_std = cones.len();
206 clarabel_standard_constraint_topp3(
207 problem,
208 &s,
209 (&mut row, &mut col, &mut val, &mut b, &mut cones),
210 num_stationary,
211 &verboser,
212 )?;
213 if verboser.is_enabled(Verbosity::Trace) {
214 crate::verbosity_log!(
215 crate::diag::Verbosity::Summary,
216 "topp3_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
217 row.len() - row_before_std,
218 col.len() - col_before_std,
219 val.len() - val_before_std,
220 b.len() - b_before_std,
221 cones.len() - cones_before_std
222 );
223 }
224 let row_before_qp = row.len();
226 let col_before_qp = col.len();
227 let val_before_qp = val.len();
228 let b_before_qp = b.len();
229 let cones_before_qp = cones.len();
230 clarabel_constraint_topp3_qp(
231 (&mut row, &mut col, &mut val, &mut b, &mut cones),
232 (idx_s_start, idx_s_final),
233 num_stationary,
234 id_xi_start,
235 id_eta_start,
236 );
237 if verboser.is_enabled(Verbosity::Trace) {
238 crate::verbosity_log!(
239 crate::diag::Verbosity::Summary,
240 "topp3_socp: qp-aux delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
241 row.len() - row_before_qp,
242 col.len() - col_before_qp,
243 val.len() - val_before_qp,
244 b.len() - b_before_qp,
245 cones.len() - cones_before_qp
246 );
247 }
248
249 let n_var = id_eta_start + len_xi;
251 let row_len = row.len();
252 let col_len = col.len();
253 let val_len = val.len();
254 let b_len = b.len();
255 let cones_len = cones.len();
256 let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
257 let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
259 let q_object = clarabel_q_object_topp3_qp(&s, num_stationary, n_var, id_eta_start);
260 if verboser.is_enabled(Verbosity::Trace) {
261 let (q_min, q_max) = q_object
262 .iter()
263 .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
264 (mn.min(v), mx.max(v))
265 });
266 crate::verbosity_log!(
267 crate::diag::Verbosity::Summary,
268 "topp3_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}, q_range=[{}, {}]",
269 b_len,
270 n_var,
271 a_csc.nnz(),
272 p_object.nnz(),
273 q_min,
274 q_max
275 );
276 }
277 if verboser.is_enabled(Verbosity::Summary) {
278 crate::verbosity_log!(
279 crate::diag::Verbosity::Summary,
280 "topp3_socp: ready to solve with row/col/val/b/cones = {row_len}/{col_len}/{val_len}/{b_len}/{cones_len} and n_var = {n_var}.",
281 );
282 }
283 let settings = options.clarabel_settings().clone();
285 let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
286 .map_err(|e| CoppError::ClarabelSolverError("topp3_socp".into(), e))?;
287 solver.solve();
288 let solution = solver.solution;
289 if verboser.is_enabled(Verbosity::Summary) {
290 crate::verbosity_log!(
291 crate::diag::Verbosity::Summary,
292 "topp3_socp: solve done, status = {:?}, elapsed = {}.",
293 solution.status,
294 format_duration_human(verboser.elapsed())
295 );
296 }
297 if verboser.is_enabled(Verbosity::Trace) {
298 let show = solution.x.len().min(3);
299 crate::verbosity_log!(
300 crate::diag::Verbosity::Summary,
301 "topp3_socp: solution x_len={}, head={:?}",
302 solution.x.len(),
303 &solution.x[0..show]
304 );
305 }
306 let result = if options.is_allow(solution.status) {
307 let (a, b) =
308 clarabel_to_copp3_solution(&solution.x.as_slice()[0..2 * (n + 1)], &s, num_stationary);
309 Some((a, b, num_stationary))
310 } else {
311 None
312 };
313 if verboser.is_enabled(Verbosity::Trace) {
314 crate::verbosity_log!(
315 crate::diag::Verbosity::Summary,
316 "topp3_socp: allow(status)={}, extracted_profile={}",
317 options.is_allow(solution.status),
318 if result.is_some() {
319 "Some((a,b,num_stationary))"
320 } else {
321 "None"
322 }
323 );
324 }
325 Ok((result, solution))
326}
327
328#[inline(always)]
330fn length_xi_eta(n: usize, num_stationary: (usize, usize)) -> usize {
331 n + 1 - num_stationary.0.max(1) - num_stationary.1.max(1)
332}
333
334#[inline(always)]
336fn skip_a_for_xi(num_stationary_start: usize) -> usize {
337 num_stationary_start.max(1)
338}
339
340fn clarabel_constraint_topp3_qp(
346 constraints: ConstraintsClarabel,
347 idx_s_interval: (usize, usize),
348 num_stationary: (usize, usize),
349 id_xi_start: usize,
350 id_eta_start: usize,
351) {
352 let (idx_s_start, idx_s_final) = idx_s_interval;
353 let n = idx_s_final - idx_s_start;
354 let (row, col, val, b, cones) = constraints;
357 let len_xi = length_xi_eta(n, num_stationary);
362 let k_skip = skip_a_for_xi(num_stationary.0);
363 row.extend(b.len()..(b.len() + len_xi));
366 col.extend(id_xi_start..(id_xi_start + len_xi));
367 val.resize(val.len() + len_xi, -1.0);
368 b.resize(b.len() + len_xi, 0.0);
369 row.extend(b.len()..(b.len() + len_xi));
372 col.extend(id_eta_start..(id_eta_start + len_xi));
373 val.resize(val.len() + len_xi, -1.0);
374 b.resize(b.len() + len_xi, 0.0);
375 cones.push(NonnegativeConeT(2 * len_xi));
376 for k in 0..len_xi {
379 row.resize(row.len() + 2, b.len());
381 col.extend([id_xi_start + k, id_eta_start + k]);
382 val.extend([-1.0, -1.0]);
383 b.push(0.0);
384 row.resize(row.len() + 2, b.len());
386 col.extend([id_xi_start + k, id_eta_start + k]);
387 val.extend([-1.0, 1.0]);
388 b.push(0.0);
389 b.push(2.0);
391 }
392 for k in 0..len_xi {
396 row.push(b.len());
398 col.push(k_skip + k);
399 val.push(-1.0);
400 b.push(1.0);
401 row.push(b.len());
403 col.push(k_skip + k);
404 val.push(-1.0);
405 b.push(-1.0);
406 row.push(b.len());
408 col.push(id_xi_start + k);
409 val.push(-2.0);
410 b.push(0.0);
411 }
412 cones.resize(cones.len() + 2 * len_xi, SecondOrderConeT(3));
413}
414
415#[inline(always)]
417fn clarabel_q_object_topp3_qp(
418 s: &[f64],
419 num_stationary: (usize, usize),
420 n_var: usize,
421 id_eta_start: usize,
422) -> Vec<f64> {
423 let mut q_object = Vec::<f64>::with_capacity(n_var);
424 let weight = get_weight_a_topp3(s, num_stationary);
425 let len_eta = length_xi_eta(s.len() - 1, num_stationary);
426 let k_skip = skip_a_for_xi(num_stationary.0);
427 q_object.resize(id_eta_start, 0.0);
428 q_object.extend(weight[k_skip..(k_skip + len_eta)].iter());
429 q_object.resize(n_var, 0.0);
430 q_object
431}
432
433#[inline(always)]
437fn clarabel_capacity_topp3_qp(n: usize) -> (usize, usize, usize) {
438 let len_xi_upper_bound = n + 1;
446 (
447 9 * len_xi_upper_bound,
448 8 * len_xi_upper_bound,
449 2 * len_xi_upper_bound + 1,
450 )
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::copp::copp2::stable::basic::{Topp2ProblemBuilder, s_to_t_topp2};
457 use crate::copp::copp2::stable::reach_set2::{ReachSet2Options, ReachSet2OptionsBuilder};
458 use crate::copp::copp2::stable::topp2_ra::topp2_ra;
459 use crate::copp::copp3::stable::basic::{Topp3ProblemBuilder, s_to_t_topp3};
460 use crate::copp::{ClarabelOptions, ClarabelOptionsBuilder};
461 use crate::path::add_symmetric_axial_limits_for_test;
462 use crate::robot::robot_core::Robot;
463 use crate::solver::topp3_lp::topp3_lp;
464 use core::f64;
465 use nalgebra::DMatrix;
466 use rand::RngExt;
467 use std::time::Instant;
468
469 #[test]
470 fn test_topp3_lp_qp() -> Result<(), CoppError> {
471 run_test_topp3_lp_qp_repeated(1, false)
472 }
473
474 #[test]
477 #[ignore = "slow"]
478 fn test_topp3_lp_qp_robust() -> Result<(), CoppError> {
479 run_test_topp3_lp_qp_repeated(100, true)
480 }
481
482 fn run_one_topp3_lp_qp_case(
483 options_ra: &ReachSet2Options,
484 options_lp: &ClarabelOptions,
485 options_qp: &ClarabelOptions,
486 ) -> Result<(f64, f64, f64, f64, f64, f64), CoppError> {
487 let n: usize = 1000;
488 let dim = 7;
489 let mut rng = rand::rng();
490 let omega = (0..dim)
491 .map(|_| rng.random_range(0.1..(2.0 * f64::consts::PI)))
492 .collect::<Vec<f64>>();
493 let phi = (0..dim)
494 .map(|_| rng.random_range(0.0..(2.0 * f64::consts::PI)))
495 .collect::<Vec<f64>>();
496
497 let mut robot = Robot::with_capacity(dim, n);
498 let s = DMatrix::<f64>::from_fn(1, n, |_, j| {
499 (j as f64
500 + (if 0 < j && 2 * j < n { 0.5 } else { 0.0 }
501 + if n > j && 2 * j > n { 0.5 } else { 0.0 })
502 * j as f64
503 / n as f64)
504 * (1.0 / (n - 1) as f64)
505 });
506 let q = DMatrix::<f64>::from_fn(dim, n, |i, j| (omega[i] * s[j] + phi[i]).sin());
507 let dq =
508 DMatrix::<f64>::from_fn(dim, n, |i, j| omega[i] * (omega[i] * s[j] + phi[i]).cos());
509 let ddq = DMatrix::<f64>::from_fn(dim, n, |i, j| {
510 -omega[i] * omega[i] * (omega[i] * s[j] + phi[i]).sin()
511 });
512 let dddq = DMatrix::<f64>::from_fn(dim, n, |i, j| {
513 -omega[i] * omega[i] * omega[i] * (omega[i] * s[j] + phi[i]).cos()
514 });
515 robot.with_s(&s.as_view())?;
516 robot.with_q(
517 &q.as_view(),
518 &dq.as_view(),
519 &ddq.as_view(),
520 Some(&dddq.as_view()),
521 0,
522 )?;
523 add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0))?;
524
525 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
526 let start = Instant::now();
527 let a_ra = topp2_ra(&topp2_problem, options_ra)?;
528 let tc_ra = start.elapsed().as_secs_f64() * 1E3;
529 let (tf_ra, _) = s_to_t_topp2(s.as_slice(), &a_ra, 0.0);
530
531 robot.constraints.amax_substitute(&a_ra, 0)?;
532 let topp3_problem = Topp3ProblemBuilder::new(&mut robot, 0, &a_ra, (0.0, 0.0), (0.0, 0.0))
533 .with_num_stationary_max(2)
534 .build_with_linearization()?;
535
536 let start = Instant::now();
537 let (a_lp, b_lp, num_stationary) = topp3_lp(&topp3_problem, options_lp)?;
538 let tc_lp = start.elapsed().as_secs_f64() * 1E3;
539 let (tf_lp, _) = s_to_t_topp3(s.as_slice(), &a_lp, &b_lp, num_stationary, 0.0);
540
541 let start = Instant::now();
542 let (a_qp, b_qp, num_stationary_qp) = topp3_socp(&topp3_problem, options_qp)?;
543 let tc_qp = start.elapsed().as_secs_f64() * 1E3;
544 let (tf_qp, _) = s_to_t_topp3(s.as_slice(), &a_qp, &b_qp, num_stationary_qp, 0.0);
545
546 Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp))
547 }
548
549 fn run_test_topp3_lp_qp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
550 let options_ra = ReachSet2OptionsBuilder::new()
551 .lp_feas_tol(1E-9)
552 .a_cmp_abs_tol(1E-9)
553 .a_cmp_rel_tol(1E-9)
554 .build()?;
555 let options_lp = ClarabelOptionsBuilder::new()
556 .allow_almost_solved(true)
557 .build()?;
558 let options_qp = ClarabelOptionsBuilder::new()
559 .allow_almost_solved(true)
560 .build()?;
561
562 let mut tc_sum_ra = 0.0;
563 let mut tc_sum_lp = 0.0;
564 let mut tc_sum_qp = 0.0;
565 let mut tf_sum_ra = 0.0;
566 let mut tf_sum_lp = 0.0;
567 let mut tf_sum_qp = 0.0;
568 let mut succeed = 0;
569
570 for i_exp in 0..n_exp {
571 if let Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp)) =
572 run_one_topp3_lp_qp_case(&options_ra, &options_lp, &options_qp)
573 {
574 if flag_print_step {
575 crate::verbosity_log!(
576 crate::diag::Verbosity::Summary,
577 "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.4}, tf_lp = {:.4}, tf_qp = {:.4}",
578 i_exp + 1,
579 tc_ra,
580 tc_lp,
581 tc_qp,
582 tf_ra,
583 tf_lp,
584 tf_qp,
585 );
586 }
587 tc_sum_ra += tc_ra;
588 tc_sum_lp += tc_lp;
589 tc_sum_qp += tc_qp;
590 tf_sum_ra += tf_ra;
591 tf_sum_lp += tf_lp;
592 tf_sum_qp += tf_qp;
593 succeed += 1;
594 }
595 }
596
597 crate::verbosity_log!(
598 crate::diag::Verbosity::Summary,
599 "Average (fail {}): tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.4}, tf_lp = {:.4}, tf_qp = {:.4}",
600 n_exp - succeed,
601 tc_sum_ra / succeed as f64,
602 tc_sum_lp / succeed as f64,
603 tc_sum_qp / succeed as f64,
604 tf_sum_ra / succeed as f64,
605 tf_sum_lp / succeed as f64,
606 tf_sum_qp / succeed as f64,
607 );
608
609 Ok(())
610 }
611}